<?php

namespace Core\Common\Base;

use Core\Common\Route\Request;

class Redis
{
    private static $pool = [];
    private $name = ''; // redis 配置名
    private $client = null;
    private $db = 0;

    private function __construct($name, $client)
    {
        $this->name = $name;
        $this->client = $client;
        $config = Config::getRedis($name);
        if ($this->client && !empty($config['db']) && $config['db'] > 0) {
            $this->client->select($config['db']);
            $this->db = $config['db'];
        }
    }

    /**
     * 获取连接在池子里的名称
     * @param $name
     * @return string
     */
    private static function getPoolKeyByName($name)
    {
        $inMars = Request::isRunningInMars();
        $poolKey = $name;
        if ($inMars) {
            $poolKey .= "_mars";
        }
        return $poolKey;
    }

    /**
     * 获取一个 redis 连接实例
     * @param $name
     * @return bool|mixed
     */
    public static function getInstance($name)
    {
        if (Request::isRunningInMars()) {
            $name = 'mars_mock';
        }
        $poolKey = self::getPoolKeyByName($name);
        if (isset(self::$pool[$poolKey])) {
            return self::$pool[$poolKey];
        }
        $client = self::connect($name);
        if (empty($client)) {
            return false;
        }

        self::$pool[$poolKey] = new self($name, $client);
        return self::$pool[$poolKey];
    }

    public static function connect($name)
    {
        $config = Config::getRedis($name);
        if (empty($config)) {
            return false;
        }

        $timeout = $config['timeout'] ?? 0;
        $host = $config['host'];
        $password = $config['password'] ?? 0;
        $port = $config['port'] ?? 6379;

        if (extension_loaded('redis')) {
            $redis = new \Redis();
            $redis->pconnect($host, $port, $timeout, $name);
            if ($password) {
                $ret = $redis->auth($password);
                if (!$ret) {
                    return false;
                }
            }
        } else {
            if (class_exists('\Predis\Client')) {
                $options = [];
                if ($password) {
                    $options = [
                        'parameters' => [
                            'password' => $password,
                        ],
                    ];
                }
                $redis = new \Predis\Client("tcp://{$host}:{$port}", $options);
            } else {
                $redis = new \Core\Common\Sys\MyRedis($host, $port, $timeout, $password);
            }
        }
        return $redis;
    }

    /**
     * 重新连接redis
     */
    private function reconnect()
    {
        $poolKey = self::getPoolKeyByName($this->name);
        unset(self::$pool[$poolKey]);

        $this->client = self::connect($this->name);
        if ($this->db > 0 && $this->client) {
            $this->client->select($this->db);

            self::$pool[$poolKey] = new self($this->name, $this->client);
        }
    }

    /**
     * 执行命令
     * @param $name
     * @param $arguments
     * @return mixed
     */
    private function retry($name, $arguments)
    {
        $t1 = microtime(true);
        $ret = $this->client->$name(...$arguments);
        $t2 = microtime(true);
        $detail = [
            'connection' => self::getPoolKeyByName($this->name),
            'cmd' => $name,
            'query' => [$name, $arguments],
            'ms' => ($t2 - $t1) * 1000,
        ];
        $this->log($detail);
        return $ret;
    }

    /**
     * 执行eval命令
     * @param $sha1
     * @param $script
     * @param $args
     * @return mixed
     */
    private function retryEval($sha1, $script, $args)
    {
        $args[0] = $sha1;
        $t1 = microtime(true);
        $ret = call_user_func_array([$this->client, 'evalsha'], $args);
        if (false === $ret) {
            // 可能是出现报错
            // NOSCRIPT No matching script. Please use EVAL.
            // 这种错误不会抛出异常
            $args[0] = $script;
            $ret = call_user_func_array([$this->client, 'eval'], $args);
        }
        $t2 = microtime(true);
        array_shift($args);
        $detail = [
            'connection' => self::getPoolKeyByName($this->name),
            'cmd' => 'eval',
            'query' => [$sha1, $args],
            'ms' => ($t2 - $t1) * 1000,
        ];
        $this->log($detail);
        return $ret;
    }

    private function evalsha()
    {
        if (empty($this->client)) {
            return null;
        }
        $args = func_get_args();
        $script = $args[0];
        $sha1 = sha1($script);
        return $this->retryEval($sha1, $script, $args);
    }

    public function __call($name, $arguments)
    {
        if (empty($this->client)) {
            return null;
        }

        $isEvalScript = 'eval' == $name;
        $tryTimes = 1;
        $ret = null;
        while ($tryTimes++ <= 2) {
            try {
                if ($isEvalScript) {
                    $ret = $this->evalsha(...$arguments);
                } else {
                    $ret = $this->retry($name, $arguments);
                }
                // 没有出现连接异常，则直接返回
                return $ret;
            } catch (\RedisException $re) {
                // 处理读数据异常或链接断开，针对 php redis 扩展做的特殊逻辑
                $haveException = $re->getMessage();
                if (empty($haveException)) {
                    $haveException = "sqkb redis gone away";
                }
                $this->errorLog([$name, $arguments], $re, $haveException);

                // 重新连接
                $this->reconnect();
            }
        }
        return $ret;
    }

    private function log($detail)
    {
        $logLevel = Log::currentLogLevel();
        $slow = Config::config('redis.slow.ms');
        $config = Config::getRedis($this->name);
        if (isset($config['add_log']) && $config['add_log'] == 0) {
            return;
        }
        if (isset($detail['ms']) && $slow && $detail['ms'] >= $slow) {
            $tagsInflux = [
                'service' => CORE_NAME,
                'uri' => Request::uri()
            ];
            $fieldsInflux = [
                'cmd' => $detail['cmd'],
                'ms' => round($detail['ms'], 3),
                'trace_id' => Request::traceId(),
            ];
            Log::influxMeasureLog('redis_slow_log', $tagsInflux, $fieldsInflux);
        }
        if ($logLevel > LOG_INFO) {
            Log::redis(LOG_INFO, $detail);
            return;
        }
        $key = $detail['query'][1][0] ?? '';
        if ($key && isset($config['key_filter'])) {
            $filters = explode(',', $config['key_filter']);
            foreach ($filters as $filter) {
                if ($filter && substr($key, 0, strlen($filter)) === $filter) {
                    Log::redis_filter(LOG_INFO, $detail);
                    return;
                }
            }
        }
    }

    /**
     * 记录错误日志
     * @param $query
     * @param $ex
     */
    private function errorLog($query, $ex, $msg = '')
    {
        $detail = [
            'error_type' => 'redis',
            'error_source' => self::getPoolKeyByName($this->name),
            'query' => $query,
            'msg' => $msg,
        ];
        $context = [
            'error_code' => $ex->getCode(),
            'message' => $ex->getMessage(),
            'trace' => $ex->getTrace(),
        ];
        Log::error(LOG_ERR, $detail, $context);
    }
}
